package com.ddwl.common.aop;

import cn.hutool.core.util.StrUtil;
import com.ddwl.common.annotation.Limit;
import com.ddwl.common.constant.LimitType;
import com.ddwl.common.constant.LimitWay;
import com.ddwl.common.constant.OperateLogEnum;
import com.ddwl.common.exception.IllegalException;
import com.ddwl.common.feign.api.DataApi;
import com.ddwl.common.util.HttpUtil;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * @author zach
 */
@Slf4j
@Aspect
@Configuration
public class LimitInterceptor {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private DataApi dataApi;

    private static String methodLimitPrefixKey = "method:limit:prefix:";
    /**
     * @param pjp
     * @author fu
     * @description 切面
     * @date 2020/4/8 13:04
     */
    @Around("execution(public * *(..)) && @annotation(com.ddwl.common.annotation.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        LimitWay limitWay = limitAnnotation.limitWay();
        String name = limitAnnotation.name();
        String key;
        Integer code=0;
        String prefix = limitAnnotation.prefix();
        if(StrUtil.isEmpty(prefix)){
            prefix = methodLimitPrefixKey;
        }
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        /**
         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
         */
        switch (limitType) {
            case IP:
                key = HttpUtil.getIp(request);
                code= OperateLogEnum.SearchPropertyEnum.IP.getCode();
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            case DK:
                key = request.getHeader("dk");
                code= OperateLogEnum.SearchPropertyEnum.DK.getCode();
                break;
            case CLASS_METHOD:
                key = getLockServiceKey(pjp);
                break;
            default:
                key = getLockServiceKey(pjp);
        }
        ImmutableList<String> keys = ImmutableList.of(String.join(prefix, key));
        try {
            int limitCount = limitAnnotation.count();
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number count = null;
            if (LimitWay.CACHE == limitWay) {
                int limitPeriod = limitAnnotation.period();
                count = redisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            }
            if (LimitWay.DB == limitWay) {
                count=dataApi.countOperateLog(code,key,getLockServiceKey(pjp)).getResult();
            }
            log.info("Access try count is {} for name={} and key = {}", count, name, key);
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                if(StrUtil.isNotBlank(limitAnnotation.errMsg())){
                    throw new IllegalException(limitAnnotation.errMsg());
                }else {
                    throw new IllegalException("超过调用限制");
                }
            }
        } catch (Throwable e) {
            if (e instanceof IllegalException) {
                throw new IllegalException(e.getLocalizedMessage());
            }
            throw e;
        }
    }

    /**
     * @author fu
     * @description 编写 redis Lua 限流脚本
     * @date 2020/4/8 13:24
     */
    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
//        lua.append("\nc = redis.call('get',KEYS[1])");
//        // 超过limitCount，则直接返回
//        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
//        lua.append("\nreturn c;");
//        lua.append("\nend");
        // 不大于 limitCount, 执行计算器自加
        lua.append("\nc = redis.call('incr',KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流，设置对应键值的过期
        lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }

    /**
     * 获取切面调用的方法名和类名
     * @param joinPoint
     * @return
     */
    private String getLockServiceKey(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = methodSignature.getName();
   //     String simpleClassName = className.split("\\.")[className.split("\\.").length - 1];
        return className + "." + methodName;
    }
}
